📝 Use Pages
📎 The Page Model
Camomilla has it's own page model. The page model has an attribute for every relevant data in a web page. It takes care of SEO data and permalinks and template, and exposes jsonfields to manage additional data.
Pages have a publish lifecycle 🌱
A page's visibility is derived from timestamps — published_at (per language: when it goes live) and a global deleted_at (soft-delete) — rather than a status column. Pages also come with drafts, scheduling, preview, and optional revisions: a new page starts as a draft and isn't served publicly until published. See 🌱 Use Page Lifecycle for the full workflow.
To use camomilla pages you need to add the dynamic url resolver at the end of your website urlpatterns:
# <project_name>/urls.py
urlpatterns += path('', include("camomilla.dynamic_pages_urls"))To handle multilanguage pages use:
# <project_name>/urls.py
urlpatterns += i18n_patterns(
path('', include("camomilla.dynamic_pages_urls")),
prefix_default_language=False
)This resolver is made to check any permalink that does not match anything in the url pattern and look in the database for a page with that permalink.
Beware!
The dynamic_pages_urls handler should always be the last handler of your urlpatterns list.
Choose the template
The page model has a field that determines which html template to use for rendering. The default value is defaults/pages/default.html. If a value is stored in this field it will be used. The rendering part is yelded by the default django template engine. The default value can be changed with a setting:
# <project_name>/settings.py
CAMOMILLA = {
"RENDER": { "PAGE": {"DEFAULT_TEMPLATE": "website/my_template.html" }}
}Add template data
Beware!
🧩 Use Page Context to inject context in a more safe way.
The data that will be available in the rendering engine should be saved in the template_data field. This field is a django JSONField. So it will accept any json structure. The data in this field will be accessible inside the template through page.template_data. If you need to enrich the template context with other data that are not stored in the template data you can provide an inject_context function in camomilla settings:
# <project_name>/settings.py
# import from external file
from my_project_app.template_rendering import page_inject_context
# or define explicitly in settings
def page_inject_context(request, super_ctx):
from my_project_app.models import Category
return {"all_categories": Category.objects.all()} # the all_category values will be accessible inside the template context.
CAMOMILLA = {
"RENDER": { "PAGE": {"INJECT_CONTEXT": page_inject_context }}
}Typed template_data
By default template_data is a plain JSONField accepting any structure. For richer editing you can redeclare it on a custom page model with a StructuredJSONField schema: the admin gets a structured form, the API gets validated payloads, and URL fields can localize themselves.
from typing import List, Optional
from camomilla.models import AbstractPage
from camomilla.types import Permalink
from structured.fields import StructuredJSONField
from structured.pydantic.models import BaseModel
class HeroBlock(BaseModel):
headline: str = ""
cta_label: str = ""
# Permalink localizes itself to the active language on the way out.
cta: Optional[Permalink] = None
class FeatureBlock(BaseModel):
icon: str = ""
title: str = ""
class HomePageData(BaseModel):
hero: HeroBlock = HeroBlock()
features: List[FeatureBlock] = []
def _home_default():
return HomePageData()
class HomePage(AbstractPage):
template_data = StructuredJSONField(schema=HomePageData, default=_home_default)
class PageMeta:
default_template = "website/pages/home.html"Use camomilla.types.Permalink for any field that stores a navigation target. Its url computed field resolves to the active-language routerlink, so on /it/ a link to the about page comes back as /it/about/ with no work on the consumer's side:
{% if page.template_data.hero.cta %}
<a href="{{ page.template_data.hero.cta.url }}">{{ page.template_data.hero.cta_label }}</a>
{% endif %}Register translations
If you want template_data (and the other inherited fields) translated per-language on a custom page model, register it with AbstractPageTranslationOptions — see 🌍 Use Modeltranslation. Otherwise modeltranslation falls back to the single base column and different locales overwrite each other on save.
Localizing raw-string permalinks in templates
When template_data stays a plain JSONField (no schema) and you store a navigation target as a bare permalink string (e.g. "/about"), the localized_url template tag resolves it to the active-language URL at render time:
{% load camomilla_filters %}
<a href="{% localized_url page.template_data.cta_url %}">CTA</a>It looks up the matching UrlNode and returns its routerlink (so /about becomes /it/about/ while Italian is active); strings that don't resolve — typos, external links, mailto: — pass through unchanged. When a request is in the template context (the standard request context processor puts it there), the returned URL is absolute; otherwise it's root-relative.
Set the permalink
By default camomilla Page autogenerates it's permalink by slugification of the SEO title. If the page has a parent_page field and a page model associated with it, the permalink will be generated as a concatenation of the parent page permalink and the current page slug.
For example to generate the permalink page_1/page_2 you will need to create 2 Pages, one with title Page 1 and one with the title Page 2 then set the first page as the parent page of the second and save it.
Otherwise, you can set the permalink manually by setting the permalink field. Manual permalink modification is available only if autopermalink field is set to False. While editing in django admin, if the user manually sets the permalink, the autopermalink field will be set to False automatically.
🖇️ The AbstractPage Model
Camomilla comes with an AbstractPage model. AbstractPage is different from Page model, it is Abstract. This means that the AbstractPage by itself does not create a database table. The only whay an AbstractPage can have its own db table is to be inherited by a concrete model like this:
from camomilla.models import AbstractPage
class MyPageModel(AbstractPage):
# ... custom logic there
passThe AbstractPage contains all the logics of Page model. This means that you can override anything and define multiple custom page models. This is very usefull if you need to build complex sites, where you can have also other kid of data that need to be rendered and treated like a page (es. product, category, blog, anything..).
You can also define some intresting properties when you are defining your own page model.:
Define page options with PageMeta
Many "settings" of the page model are stored inside a PageMeta class like this:
from camomilla.models import AbstractPage
class MyPageModel(AbstractPage):
class PageMeta:
parent_page_field = "parent_page"
default_template = "website/my_template.html"
def inject_context_func(request, super_ctx):
from my_project_app.models import Category
return {"all_categories": Category.objects.all()}From PageMeta class you can define:
parent_page_field==> set a different propery to store page parent. Likecategoryor anything else.default_template==> set a different default template.inject_context_func==> add more data inside template context
Beware!
🧩 Use Page Context to inject context in a more safe way.
Override the page context
This has almost the same functionality of inject_context_func PageMeta option but it runs at a more deep level. Overriding this will override the context and not only add things to it.
from camomilla.models import AbstractPage
class MyPageModel(AbstractPage):
def get_context(request):
from my_project_app.models import Category
return {"page": self, "all_categories": Category.objects.all()}We suggest to always return { "page": self } in the context.
🌐 Build a sitemap.xml
To build a sitemap.xml you can use the standard django sitemap framework. Camomilla comes with a CamomillaPageSitemap class that you can use to include camomilla pages in your sitemap.xml.
# <project_name>/sitemap.py
from django.contrib.sitemaps import Sitemap
from camomilla.sitemaps import CamomillaPageSitemap
# declare your custom sitemaps
class StaticViewSitemap(Sitemap):
def items(self):
return ['home', 'about', 'contact']
def location(self, item):
return reverse(item)
sitemaps = {
'pages': CamomillaPageSitemap, # add the camomilla sitemap
'static': StaticViewSitemap, # add your custom sitemaps to the camomilla sitemaps
}# <project_name>/urls.py
from django.contrib.sitemaps.views import sitemap
from .sitemap import sitemaps
urlpatterns += [
path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap')
]To customize the camomilla sitemap you can override the CamomillaPageSitemap class. Overriding this class will allow you to customize changefreq, priority and also items.
from camomilla.sitemaps import CamomillaPageSitemap
class MyCamomillaPageSitemap(CamomillaPageSitemap):
changefreq = "monthly"
priority = 0.5
def items(self):
return super().items().filter(...)Lifecycle-aware filtering
There is no status database column — a page's lifecycle is derived from its timestamps. On page querysets .filter(status="PUB") / .exclude(status="TRS") / .filter(is_public=True) still work (the manager rewrites them into timestamp conditions), and the explicit helpers .public() / .alive() / .trashed() / .draft() / .scheduled() express the same intent more clearly. (order_by/values("status") need .with_lifecycle().) Note the sitemap's items() yields UrlNode objects (see the note below), not pages. See 🌱 Use Page Lifecycle.
Remember that items is a queryset of UrlNode objects and to access the page model you need to use the page property of the UrlNode object.
🗂️ Pages router API endpoint
Beware!
If you need to create an api endpoint for a model inheriting from the AbstractPage model remember to use inside the serializer the AbstractPageMixin.
Camomilla comes with a builtin pages router endpoint that allows you to browse your pages by url.
URL Structure:
api/camomilla/pages-router/<page_url>
You provide as page_url the full public path of the page, including any language prefix — exactly the URL a visitor would type. The router derives the language from that prefix (the same way Django's i18n_patterns does on the HTML route), looks up the page, and serves it already translated into that language:
/api/camomilla/pages-router/about→ the about page in the default language/api/camomilla/pages-router/it/about→ the about page in Italian
When no language prefix is present, the default language is used. If the requested permalink doesn't exist in the resolved language, the endpoint returns a 404.
Canonical redirects
The router enforces canonical URLs. A request whose form differs from the canonical one (missing trailing slash, bare /it instead of /it/, etc.) returns a redirect descriptor in the body — {"redirect": "/it/about/", "status": 301} — rather than the page payload, so external renderers stay on a single canonical URL per page.
Editor previews
pages-router always serves the public state of a page — trashed, draft and scheduled pages return 404. To preview unpublished content (with any pending draft overlaid), authenticated editors use the mirror endpoint api/camomilla/pages-router-preview/<page_url>, which bypasses the public gate. See 🌱 Use Page Lifecycle for the full drafting / preview / scheduling workflow.
Simple Response:
{
"id": 1,
"is_public": false,
"status": "DRF",
"indexable": true,
"alternates": {
"it": "/",
"en": "/en/"
},
"permalink": "/",
"related_name": "camomilla_page",
"breadcrumbs": [
{
"permalink": "/",
"title": null
}
],
"routerlink": "/",
"template": "website/home.html",
"title": null,
"description": null,
"og_description": null,
"og_title": null,
"og_type": null,
"og_url": null,
"canonical": null,
"meta": {},
"date_created": "2023-09-07T13:16:24.762269Z",
"date_updated_at": "2023-09-08T17:38:03.155480Z",
"breadcrumbs_title": null,
"slug": null,
"template_data": {},
"identifier": "4ee68c88-dd1a-4515-bf50-7839f2cf1e72",
"pubblication_date": null,
"ordering": 0,
"og_image": null,
"url_node": {
"id": 46,
"permalink": "/",
"related_name": "camomilla_page"
},
"parent_page": null
}The respose will not include field translations since the response has the content already translated in the active language. But if you need also some other language you can specify the included_translations parameter in the request.
/api/camomilla/pages-router/<page_url>?included_translations=it
The included_translations parameter accepts a comma separated list of languages or the value all to include all the translations.